/*
 * Copyright (C) 2010 TopCoder Inc., All Rights Reserved.
 */
package com.hercules.xre.protocol.testing.netty;

import java.util.regex.Matcher;
import java.util.regex.Pattern;

import org.jboss.netty.channel.ChannelHandlerContext;
import org.jboss.netty.channel.ChannelPipelineCoverage;
import org.jboss.netty.channel.MessageEvent;
import org.jboss.netty.channel.SimpleChannelUpstreamHandler;

import com.hercules.xre.protocol.testing.EventHandler;
import com.hercules.xre.protocol.testing.EventHandlerException;
import com.hercules.xre.protocol.testing.Helper;
import com.topcoder.util.log.Level;
import com.topcoder.util.log.Log;
import com.topcoder.util.log.LogManager;

/**
 * <p< Channel handler that is responsible for parsing and handling of passed XRE events. Events are read from passed
 * event message thus this handler should be proceeded in the pipeline by other handler that will decode passed message
 * and pass string with serialized events to this handler (ie. TCPMessageHandler or HTTPRequestHandler). </p>
 * <p>
 * <strong>Thread-safety:</strong> This class uses synchronized collections in order to assure thread safety.
 * </p>
 *
 * @author TCSDESIGNER, TCSDEVELOPER
 * @version 1.0
 */
@ChannelPipelineCoverage("one")
public class XREEventsHandler extends SimpleChannelUpstreamHandler {
    /**
     * <p>
     * Logger that is used to log activities and errors. It is initialized to
     * LogManager.getLog(XREEventHandler.class.getName()). Can not be null, never changes after initialization.
     * </p>
     */
    private static final Log LOGGER = LogManager.getLog(XREEventsHandler.class.getName());

    /**
     * <p>
     * Pattern that is used to match "name" in the command string. It is initialized to
     * Pattern.compile("name[\\s]*:[\\s]*[\\w\\p{Punct}]+"). Can not be null, never changes after initialization.
     * </p>
     */
    private static final Pattern EVENT_NAME_PATTERN = Pattern.compile("\"name\"[\\s]*:\"[\\s]*[\\w\\p{Punct}]+\"");

    /**
     * <p>
     * Event handler that is used to inform handle XRE events. Set in the constructor, never changes after that. Can't
     * be null.
     * </p>
     */
    private final EventHandler eventHandler;

    /**
     * <p>
     * Connection manager that will be used in the event handler. It is set in the constructor and never changes after
     * that.
     * </p>
     */
    private final BaseProtocolTestingConnectionManager connectionManager;

    /**
     * <p>
     * Creates new instance.
     * </p>
     *
     * @param eventHandler event handler that will be used to report detected events, can't be null
     * @param connectionManager connection manager, can't be null
     * @throws IllegalArgumentException when any attribute is invalid
     */
    public XREEventsHandler(BaseProtocolTestingConnectionManager connectionManager, EventHandler eventHandler) {
        final String signature = "XREEventsHandler#ctor";
        Helper.logEnter(signature, new String[] {"connectionManager", "callback"}, new Object[] {connectionManager,
            eventHandler}, LOGGER);
        try {
            Helper.checkNull(connectionManager, "connectionManager", signature, LOGGER);
            Helper.checkNull(eventHandler, "eventHandler", signature, LOGGER);
            this.connectionManager = connectionManager;
            this.eventHandler = eventHandler;
        } finally {
            Helper.logExit(signature, null, LOGGER);
        }
    }

    /**
     * <p>
     * Method called when new message has been received and decoded. This method assumes that passed message is a
     * string with serialized events. After de-serialization events are dispatched using provided eventHandler.
     * </p>
     * <p>
     * After handling all events handler passes control to the next handler in pipeline in order to allow for
     * preparation of response (used for HTTP based communication).
     * </p>
     *
     * @param e message event, can't be null
     * @param ctx channel handler context, can't be null
     * @throws IllegalArgumentException when any of parameters is invalid.
     * @throws IllegalMessageException when passed message isn't instance of a string.
     * @throws NoSuchSessionException if there is no such session
     */
    public void messageReceived(ChannelHandlerContext ctx, MessageEvent e) throws NoSuchSessionException,
            IllegalMessageException {
        final String signature = "XREEventsHandler#messageReceived";
        Helper.logEnter(signature, new String[] {"ChannelHandlerContext", "MessageEvent"}, new Object[] {ctx, e},
                LOGGER);
        try {
            Helper.checkNull(ctx, "ctx", signature, LOGGER);
            Helper.checkNull(e, "message event", signature, LOGGER);
            Object obj = e.getMessage();
            if (!(obj instanceof String)) {
                throw Helper.logError(new IllegalMessageException("the message is not String"), signature, LOGGER);
            }
            String eventsString = ((String) obj).trim();
            if (eventsString.startsWith("[")) {
                // the message starts with "[", we consider this as an array of events
                eventsString = eventsString.substring(1, eventsString.length() - 1).trim();
                int startIndex = 0;
                int endIndex = 0;
                int braceCount = 0;
                int curIndex;
                while (eventsString.length() > 1) {
                    startIndex = eventsString.indexOf("{");
                    braceCount = 1;
                    curIndex = startIndex;
                    while (braceCount != 0 && curIndex < eventsString.length()) {
                        curIndex++;
                        if (eventsString.charAt(curIndex) == '}') {
                            braceCount--;
                        } else if (eventsString.charAt(curIndex) == '{') {
                            braceCount++;
                        }
                    }
                    endIndex = curIndex;
                    String eventString = eventsString.substring(startIndex, endIndex + 1);
                    handleEvent(eventString, ctx);
                    eventsString = eventsString.substring(endIndex + 1);
                }
            } else {
                // we consider this as a single event
                handleEvent(eventsString, ctx);
            }
            // send the channel event upstream in order to allow for other handlers to react
            // (for example if HTTPConnectionManager is used, HTTPResponse should be prepared and send)
            ctx.sendUpstream(e);
        } catch (NoSuchSessionException ex) {
            throw Helper.logError(ex, signature, LOGGER);
        } catch (IndexOutOfBoundsException ex) {
            throw Helper.logError(new IllegalMessageException("The event String is invalid", ex), signature, LOGGER);
        } finally {
            Helper.logExit(signature, null, LOGGER);
        }
    }

    /**
     * <p>
     * Handles a single event string. This method will try to get sessionGUID from the event string, if not set assign
     * a new one, and pass the event to the eventHandler.
     * </p>
     *
     * @param ctx channel handler context, can't be null
     * @param eventString the string representation of an event.
     * @throws IllegalArgumentException when passed eventString is null/empty
     * @throws NoSuchSessionException if the sessionGUID of the eventString doesn't correspond to a valid session.
     * @throws IllegalMessageException if the event string is invalid
     */
    private void handleEvent(String eventString, ChannelHandlerContext ctx) throws NoSuchSessionException,
            IllegalMessageException {
        final String signature = "XREEventsHandler#handleEvent";
        Helper.logEnter(signature, new String[] {"eventString", "ctx"}, new Object[] {eventString, ctx}, LOGGER);
        try {
            Helper.checkString(eventString, "eventString", signature, LOGGER);
            Helper.checkNull(ctx, "ctx", signature, LOGGER);
            Matcher matcher = EVENT_NAME_PATTERN.matcher(eventString);
            if (!matcher.find()) {
                throw Helper.logError(new IllegalMessageException(
                        "Can not find EVENT_NAME_PATTERN based on the event String"), signature, LOGGER);
            }
            String eventName = eventString.substring(matcher.start(), matcher.end()).split(":")[1].trim();
            if ("\"onConnect\"".equals(eventName)) {
                String sessionGUID = com.hercules.xre.protocol.testing.runner.Constants.SESSION_GUID;
                connectionManager.addChannelSessionGUIDAssociation(ctx.getChannel(), sessionGUID);
                eventHandler.handleConnectionCreated(eventString);
                LOGGER.log(Level.INFO, "accepted client connection, sessionGUID : " + sessionGUID);
            } else if ("\"onShutdown\"".equals(eventName)) {
                String sessionGUID = connectionManager.getSessionGUIDByChannel(ctx.getChannel());
                eventHandler.handleConnectionShutdown(sessionGUID, eventString);
                LOGGER.log(Level.INFO, "close client connection, sessionGUID : " + sessionGUID);
            } else {
                String sessionGUID = connectionManager.getSessionGUIDByChannel(ctx.getChannel());
                eventHandler.handleConnectionEvent(sessionGUID, eventString);
                LOGGER.log(Level.TRACE, "handle client connection, sessionGUID : " + sessionGUID);
            }
        } catch (EventHandlerException e) {
            Helper.logError(e, signature, LOGGER);
        } finally {
            Helper.logExit(signature, null, LOGGER);
        }
    }
}
